/**
 * KopsoxApplicationServices
 * Copyright (C) 2010 Free Software Foundation, Inc. <http://fsf.org/>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kopsox.services.application;

import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.kopsox.services.security.KopsoxPrincipal;
import org.kopsox.services.security.PrincipalGroup;
import org.kopsox.services.security.Role;

/**
 * This class manages application data like the session
 * 
 * @author Konrad Renner
 *
 */
public final class Application {

	private final static Application INSTANCE = new Application();
	
	private Session session;
	
	private Application() {
		//nothing
	}
	
	public static final Session getSession() {
		if(INSTANCE.session == null) {
			throw new IllegalStateException("No Session is initialized");
		}
		
		return INSTANCE.session;
	}
	
	/**
	 * Initializes the Application session
	 * 
	 * @param user
	 * @param timeToLive
	 * @param canTimeout
	 * @author Konrad Renner
	 */
	public static final synchronized void initializeSession(Principal user, long timeToLive, boolean canTimeout) {
		if(INSTANCE.session == null) {
			INSTANCE.session = new ApplicationSession(user,timeToLive,canTimeout);
		}
	}
	
	/**
	 * Initializes the Application session with the given session
	 * 
	 * @param session
	 * @author rpri182 - RenK
	 */
	public static final synchronized void initializeSession(Session session) {
		if(INSTANCE.session == null) {
			INSTANCE.session = session;
		}
	}
	
	
	private static final class ApplicationSession implements Session{
		
		/**
		 * serialVersionUID = 6025660870031680175L
		 */
		private static final long serialVersionUID = 6025660870031680175L;
		
		private final Principal user;
		private final long timeToLive;
		private volatile long startTime;
		//Cache for "Kopsox"-Users
		private final KopsoxPrincipal kopsoxUser;
		private final Map<String, Object> sessionAttributes;
		private final int hashCode;
		private final boolean canSessionTimeout;
		
		protected ApplicationSession(Principal user,long time,boolean canSessionTimeout) {
			if(user instanceof KopsoxPrincipal) {
				kopsoxUser = (KopsoxPrincipal)user;
			}else {
				kopsoxUser = null;
			}
			this.canSessionTimeout = canSessionTimeout;
			this.user = user;
			this.timeToLive = time;
			this.sessionAttributes = new ConcurrentHashMap<String, Object>();
			
			int prime = 31;
			int result = 1;
			this.hashCode = prime * result + ((user == null) ? 0 : user.hashCode());
			this.startTime = System.currentTimeMillis();
		}
		
		
		@Override
		public boolean reactivateSession() {
			if(!isSessionAlive()) {
				return manipulateSessionTimeout(true);
			}
			return false;
		}



		@Override
		public boolean passivateSession() {
			if(isSessionAlive()) {
				return manipulateSessionTimeout(false);
			}
			return false;
		}
		
		private final synchronized boolean manipulateSessionTimeout(boolean reactivate) {
			//Double Check Idiom
			if(reactivate && !isSessionAlive()) {
				this.startTime = System.currentTimeMillis();
			}else if(!reactivate && isSessionAlive()) {
				this.startTime = Long.MIN_VALUE;
			}
			return false;
		}


		@Override
		public String toString() {
			
			StringBuilder sb = new StringBuilder();
			sb.append("ApplicationSession [user=");
			sb.append(user);
			sb.append(", kopsoxUser=");
			sb.append(kopsoxUser);
			sb.append(", timeToLive=");
			sb.append(timeToLive);
			sb.append(", startTime=");
			sb.append(startTime);
			sb.append(", canSessionTimeout=");
			sb.append(canSessionTimeout);
			sb.append(", sessionAttributes=");
			sb.append(sessionAttributes);
			sb.append(']');
			
			return sb.toString();
		}

		@Override
		public Principal getPrincipal() {
			checkTimeout();
			checkPrincipalImplementation();
			return this.user;
		}

		@Override
		public boolean isUserInRole(Role role) {
			checkTimeout();
			checkPrincipalImplementation();
			return kopsoxUser.hasRole(role);
		}

		@Override
		public boolean isUserInGroup(PrincipalGroup group) {
			checkTimeout();
			checkPrincipalImplementation();
			return group.contains(kopsoxUser);
		}

		@Override
		public void setAttribute(String name, Object attribute) {
			checkTimeout();
			this.sessionAttributes.put(name, attribute);
		}

		@Override
		public Object getAttribute(String name) {
			checkTimeout();
			return this.sessionAttributes.get(name);
		}
		
		/**
		 * Throws an java.lang.IllegalArgumentException if the user ist not an instance of KopsoxPrincipal
		 * 
		 * @throws java.lang.IllegalArgumentException
		 * @author Konrad Renner
		 */
		private final void checkPrincipalImplementation() {
			if(kopsoxUser == null) {
				throw new java.lang.IllegalArgumentException("user must be an instance of "+KopsoxPrincipal.class.getName());
			}
		}
		
		
		@Override
		public boolean isSessionAlive() {
			return startTime > (System.currentTimeMillis()-timeToLive);
		}

		/**
		 * Throws an SessionTimeoutException if the session has expired
		 * 
		 * @throws org.kopsox.services.application.SessionTimeoutException
		 * @author Konrad Renner
		 */
		private final void checkTimeout() {
			if(canSessionTimeout && !isSessionAlive()) {
				throw new SessionTimeoutException();
			}
		}

		@Override
		public int hashCode() {
			return this.hashCode;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ApplicationSession other = (ApplicationSession) obj;
			if (user == null) {
				if (other.user != null)
					return false;
			} else if (!user.equals(other.user))
				return false;
			return true;
		}
	}
}
